package org.moonshot.util;

import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashMap;

/**
 * I implement a relatively efficient, type-safe, reflection-based, generic event broadcasting mechanism.
 * I refer only weakly to subscribed listeners, so that I do not interfere in normal garbage-collection.
 * @author Tiogshi
 *
 * @param <EventType>
 * @param <ListenerType>
 */
public class EventManager
{   
    private final ArrayList<WeakReference<EventListener>> listeners = new ArrayList<WeakReference<EventListener>> (0);
    
    private final Class<? extends EventObject> eventType;
    private final Class<? extends EventListener> listenerType;
    
    private final HashMap<String, Method> methodMap = new HashMap<String, Method> ();
    
    /**
     * Create an event manager that broadcasts events to a given type of listener on one or more methods of that listener type.
     * @param aListenerType
     */
    public EventManager (final Class<? extends EventObject> aEventType, final Class<? extends EventListener> aListenerType)
    {
        eventType = aEventType;
        listenerType = aListenerType;
        
        if( eventType == null || listenerType == null )
        {
            throw new IllegalArgumentException("EventManager requires an event type to broadcast and a listener type to broadcast to");
        }
        
        for(final Method theMethod : listenerType.getMethods () )
        {
            Class<?>[] theTypes = theMethod.getParameterTypes ();
            if( theTypes.length == 1 && theTypes[0].isAssignableFrom (eventType) )
            {
                final Method theExisting = methodMap.get (theMethod.getName ());
                
                // If there is an existing function of the same signature, prefer the more specific one.
                // Not the standard Java way, maybe, but it may help avoid unnecessary double-dispatch.
                if( theExisting == null || theExisting.getParameterTypes ()[0].isAssignableFrom (theTypes[0]) )
                {
                    methodMap.put (theMethod.getName (), theMethod);
                }
            }
        }
        
        if( methodMap.size () == 0 )
        {
            throw new IllegalArgumentException("no methods in "+listenerType+" accept one argument of type "+eventType);
        }
    }
    
    /**
     * Add a listener to this manager.
     * @param aListener
     */
    public synchronized void addListener(final EventListener aListener)
    {
    	if( listenerType.isInstance (aListener) )
    	{
    		listeners.add (new WeakReference<EventListener>(aListener));
    	}
    }
    
    /**
     * Remove a listener from this manager.
     * @param aListener
     */
    public synchronized void removeListener(final EventListener aListener)
    {
        for(WeakReference<EventListener> theListener : listeners)
        	if( theListener.get() == aListener )
        	{
        		listeners.remove(theListener);
        		break;
        	}
    }
    
    /**
     * Get the number of listeners registered to this manager.
     * @return
     */
    public int getListenerCount ()
    {
        return listeners.size ();
    }
    
    /**
     * Publish the given event instance to the method of the given name.
     * @param aMethodName
     * @param aEvent
     */
    public synchronized void publish(final String aMethodName, final EventObject aEvent)
    {
        final Method theMethod = methodMap.get (aMethodName);
        if( theMethod == null )
        {
            throw new IllegalArgumentException("cannot send "+aMethodName+"("+eventType+") to "+listenerType);
        }
        
        for(final WeakReference<EventListener> theListener: listeners)
        {
            try
            {
                theMethod.invoke (theListener.get(), aEvent);
            }
            catch (IllegalArgumentException e)
            {
                e.printStackTrace();
            }
            catch (IllegalAccessException e)
            {
                e.printStackTrace();
            }
            catch (InvocationTargetException e)
            {
                e.printStackTrace();
            }
        }
    }
}
